Fix ActiveRecord dependency check for Zeitwerk eager loading#504
Fix ActiveRecord dependency check for Zeitwerk eager loading#504crmne merged 1 commit intocrmne:mainfrom
Conversation
Adds validation that Zeitwerk can eager load all files without errors. This catches autoloading issues like inflector bugs and missing conditional checks before they reach production. - Added check after linter step for fast fail - Runs on all Ruby/Rails matrix combinations - ~2 second check prevents production crashes ## Blocked This PR depends on crmne#504 (ActiveRecord dependency fix) to pass. Without it, the eager loading check fails with NameError. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Adds a validation step that runs Zeitwerk::Loader.eager_load_all after dependency installation to catch autoloading issues early in the CI pipeline. This check helps prevent production crashes by detecting: - Inflector configuration errors - Constant definition issues - Missing conditional checks for optional dependencies The check runs quickly (~2 seconds) before the test matrix, providing rapid feedback on structural issues. Note: This check currently fails due to upstream ruby_llm ActiveRecord dependency issues. See: - crmne/ruby_llm#504 - crmne/ruby_llm#505 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for the work here. I closed #505 so we can fold it into #504 and keep this as one complete fix.
Could you please update #504 with the following?
- Add the CI eager-load check from #505 into this PR.
- We do want that guard in CI, just bundled with the code changes it depends on.
- Keep the Railtie
ActiveSupport.on_load(:active_record)integration.
- This is the right Rails hook: AR-related code only runs when ActiveRecord is actually loaded.
- Remove the top-level
if defined?(ActiveRecord::Base)wrappers in AR files (acts_as.rb,acts_as_legacy.rb,chat_methods.rb,message_methods.rb,model_methods.rb).
- Those wrappers can make a file load as a no-op if AR isn’t ready yet.
- Later requires won’t re-run the file, so constants/modules may stay undefined.
- Better pattern: define modules normally, and control when they are included via Railtie hooks.
- Add explicit requires for direct dependencies in those files (for example
require "active_support/concern"whereActiveSupport::Concernis used).
- This avoids relying on incidental load order and makes eager loading more reliable.
- Run the eager-load check in appraisal/matrix context (same Rails version under test).
- That makes sure the check validates the actual gem set for each matrix target, not just the base bundle.
Reason for all of this: we keep the Rails eager-loading fix, avoid subtle load-order traps, and keep a clean path for possible non-Rails ActiveRecord support later (CLI/Sinatra).
c024641 to
eadc24d
Compare
|
Apologies for the delay. I made the updates as requested but please let me know if I misunderstood something or if there's anything else to change. Thanks! |
eadc24d to
b808b6d
Compare
| bundle install | ||
| bundle exec appraisal ${{ matrix.rails-version }} bundle install | ||
|
|
||
| - name: Run eager-load guard (appraisal) |
There was a problem hiding this comment.
This is not really needed. you can do it in the test helper
| @@ -1,5 +1,8 @@ | |||
| # frozen_string_literal: true | |||
|
|
|||
| require 'active_support/concern' | |||
There was a problem hiding this comment.
every require should in line 4 of railtie , after if defined?(Rails::Railtie)
Then you dont have dups.
|
Looks like there are some test failures. Can you please fix them @trevorturk ? |
f72a79f to
9feb1b8
Compare
9feb1b8 to
aa73ba7
Compare
|
@crmne apologies for fighting with CI here... hopefully updated and all green now, but I'll keep an eye and try again if there are more failures after the heavy run! |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #504 +/- ##
==========================================
+ Coverage 86.13% 86.17% +0.03%
==========================================
Files 118 118
Lines 5360 5375 +15
Branches 1346 1345 -1
==========================================
+ Hits 4617 4632 +15
Misses 743 743 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
Fixes RubyLLM's optional ActiveRecord integration so the core gem can be required and Zeitwerk-eager-loaded without loading Rails-only ActiveRecord mixins too early.
The PR keeps normal Rails usage intact: in a Rails app,
acts_as_chat,acts_as_message,acts_as_model, andacts_as_tool_callare still installed onActiveRecord::Baseby the RubyLLM railtie after ActiveRecord loads.Why This Was Needed
lib/ruby_llm/active_recordis optional Rails integration code. It should not be part of RubyLLM's standalone gem eager-load path.Before this change, the gem-level Zeitwerk loader still managed
lib/ruby_llm/active_record. That meant a standalone eager-load like this could load ActiveRecord mixin files even when a Rails app had not booted ActiveRecord:That exposed load-order assumptions inside the ActiveRecord integration. The immediate CI failure was:
The failing line was in
RubyLLM::ActiveRecord::ModelMethods, which uses ActiveSupport'sdelegate. The broader issue was not just that one missing require; the optional ActiveRecord directory itself was being eager-loaded by the core gem loader.How Normal Rails Loading Works
The intended Rails path is:
ruby_llm.ruby_llmrequiresruby_llm/railtiewhenRails::Railtieis defined.RubyLLM::Railtieregisters anActiveSupport.on_load :active_recordhook.RubyLLM::ActiveRecord::ActsAsorRubyLLM::ActiveRecord::ActsAsLegacyintoActiveRecord::Base.acts_as_*APIs normally.This PR makes that boundary explicit: core RubyLLM eager-loading does not load ActiveRecord integration files; the Rails railtie loads them when ActiveRecord is available.
Changes
Isolate ActiveRecord integration from gem eager-load
lib/ruby_llm.rbnow ignoreslib/ruby_llm/active_recordin the gem-level Zeitwerk loader:This matches the existing treatment for tasks, generators, and the railtie itself. It prevents
Zeitwerk::Loader.eager_load_allfrom loading optional Rails integration code during standalonerequire "ruby_llm"usage.Stop loading ActiveRecord mixins from
ruby_llm.rblib/ruby_llm.rbno longer directly requiresruby_llm/active_record/acts_as.It only requires the railtie when Rails is present:
That keeps the Rails integration behind the railtie instead of coupling it to the core gem entrypoint.
Make the railtie explicitly own ActiveRecord integration loading
lib/ruby_llm/railtie.rbnow loads the ActiveRecord support files insideActiveSupport.on_load :active_record:ruby_llm/active_record/payload_helpersruby_llm/active_record/chat_methodsruby_llm/active_record/message_methodsruby_llm/active_record/model_methodsruby_llm/active_record/tool_call_methodsThen it loads the configured public API module:
ruby_llm/active_record/acts_aswhenRubyLLM.config.use_new_acts_asis trueruby_llm/active_record/acts_as_legacyotherwiseThis preserves normal Rails behavior while removing reliance on gem-level Zeitwerk autoloads for an ignored directory.
Make ActiveRecord files explicit about their dependencies
Several files previously relied on incidental load order or Zeitwerk autoloads. The PR adds explicit requires so each file is safe when loaded by the railtie:
acts_as.rbnow requiresactive_support/concernandactive_support/inflector.acts_as_legacy.rbnow requiresactive_support/concernandactive_support/inflector.chat_methods.rbnow requiresactive_support/concern.message_methods.rbnow requiresactive_support/concernandruby_llm/active_record/payload_helpers.model_methods.rbnow requiresactive_support/concernandactive_support/core_ext/module/delegation.payload_helpers.rbnow requiresactive_support/core_ext/object/blankandjson.tool_call_methods.rbnow requiresactive_support/concernandruby_llm/active_record/payload_helpers.These changes are intentionally small, but they are important because ignoring the ActiveRecord directory removes the previous incidental Zeitwerk autoload fallback.
Keep the standalone helper spec independent
spec/ruby_llm/active_record_tool_error_helpers_spec.rbtestsMessageMethodsandToolCallMethodswithout booting the dummy Rails app. After isolatinglib/ruby_llm/active_recordfrom the gem Zeitwerk loader, that spec must require the helper modules it exercises directly. This keeps the test independent of suite ordering and matches the new explicit-loading contract.Add CI coverage for the failure mode
The test matrix now runs an eager-load guard for each Rails appraisal after dependencies install:
This catches regressions where optional Rails integration code leaks into the standalone gem eager-load path.
Reviewer Notes
This diff is easier to review with whitespace hidden:
https://github.com/crmne/ruby_llm/pull/504/files?w=1
The code changes are mostly explicit
requirelines plus one Zeitwerk ignore. Hiding whitespace makes it easier to see that the behavioral change is the load boundary, not a broad refactor.Validation
Verified against the current PR head (
aa73ba7a) after checking out the GitHub PR branch locally.Standalone RubyLLM eager-load:
Rails appraisal eager-load guards:
Rails integration still installs
acts_as_chatthrough the railtie:Output confirmed:
Direct explicit ActiveRecord support-file loading works without relying on RubyLLM's gem Zeitwerk loader:
The CI-failing standalone helper spec also passes through
rspec-queue, matching the worker loading path from the failing job:env TEST_QUEUE_WORKERS=2 SKIP_COVERAGE=true bundle exec appraisal rails-7.1 bin/rspec-queue spec/ruby_llm/active_record_tool_error_helpers_spec.rbTargeted ActiveRecord specs:
SKIP_COVERAGE=true bundle exec appraisal rails-8.1 rspec spec/ruby_llm/active_record spec/ruby_llm/active_record_tool_error_helpers_spec.rbResult:
RuboCop on changed Ruby files:
Result:
Whitespace check:
Result: no whitespace errors.